/*
 * To change this license header, choose License Headers in Project Properties.
 * To change this template file, choose Tools | Templates
 * and open the template in the editor.
 */
package com.compomics.fspike.gui.taxonomytree;

import com.compomics.fspike.gui.taxonomytree.rendering.TaxonomyTreeIconRenderer;
import com.compomics.fspike.gui.taxonomytree.rendering.DefaultTaxonomyTreeNode;
import com.compomics.fspike.gui.taxonomytree.rendering.DefaultTaxonomyTreeModel;
import com.compomics.fspike.gui.NCBITaxonomyViewer;
import com.compomics.fspike.gui.dialog.DialogSuggestedSearchTerms;
import java.io.IOException;
import java.net.MalformedURLException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.swing.JFrame;
import javax.swing.JTree;
import javax.swing.UIDefaults;
import javax.swing.UIManager;
import javax.swing.UnsupportedLookAndFeelException;
import javax.swing.plaf.nimbus.NimbusLookAndFeel;
import javax.swing.tree.TreeNode;
import javax.swing.tree.TreePath;
import javax.swing.tree.TreeSelectionModel;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;

/**
 *
 * @author Kenneth
 */
public class NCBITaxonomyTree extends JTree {

    private final String prefix = "http://www.ncbi.nlm.nih.gov";
    private JFrame parent = null;
    private TaxonomyTreeIconRenderer taxonomyTreeIconRenderer;

    public NCBITaxonomyTree(JFrame parent) throws MalformedURLException {
        super();
        this.parent = parent;
        initTree();
    }

    public NCBITaxonomyTree() throws MalformedURLException {
        super();
        initTree();
    }

    /**
     *
     * @param root the starting point of the search
     * @param nodeName the name of the node to search for
     * @return the node bearing the nodeName or null if none existent
     */
    public DefaultTaxonomyTreeNode searchNode(DefaultTaxonomyTreeNode root, String nodeName) {
        DefaultTaxonomyTreeNode node;
        Enumeration e = root.breadthFirstEnumeration();
        while (e.hasMoreElements()) {
            node = (DefaultTaxonomyTreeNode) e.nextElement();
            if (nodeName.equals(node.getUserObject().toString())) {
                collapseFullTree();
                //return the node
                TreePath selectedPath = new TreePath(node.getPath());
                scrollPathToVisible(selectedPath);
                try {
                    getChildren(node);
                } catch (IOException ex) {
                    Logger.getLogger(NCBITaxonomyTree.class.getName()).log(Level.SEVERE, null, ex);
                }
                return node;
            }
        }
        return null;
    }

    public List<DefaultTaxonomyTreeNode> getChildren(DefaultTaxonomyTreeNode node) throws MalformedURLException, IOException {
        //get the URL from the parent
        String currentURL = node.getURLAsString();
        //parse the document with JSOUP
        Document doc = Jsoup.connect(currentURL.replace("Info", "Tree")).get();
        // get page title
        List<DefaultTaxonomyTreeNode> nodesToAdd = new LinkedList<>();
        Elements ul = doc.select("ul");
        for (Element aTopList : ul) {
            for (Element aMainEntry : aTopList.select("a")) {
                if (aMainEntry != null) {

                    String id = aMainEntry.attr("href");
                    id = id.split("id=")[1];
                    id = id.substring(0, id.indexOf("&lv"));
                    String ulText = aMainEntry.text();
                    DefaultTaxonomyTreeNode aMainNode = new DefaultTaxonomyTreeNode(ulText);
                    aMainNode.setUrl(prefix + aMainEntry.attr("href"));
                    aMainNode.setTaxonomyID(id);
                    if (ulText.equalsIgnoreCase(node.toString())) {
                        Elements li = doc.select("li");
                        for (Element aSubList : li) {
                            for (Element aSubEntry : aSubList.select("a")) {
                                if (aSubEntry != null) {
                                    String subId = aSubEntry.attr("href");
                                    subId = subId.split("id=")[1];
                                    subId = subId.substring(0, id.indexOf("&lv"));
                                    String liText = aSubEntry.text();
                                    DefaultTaxonomyTreeNode aSubNode = new DefaultTaxonomyTreeNode(liText);
                                    aSubNode.setUrl(prefix + aMainEntry.attr("href"));
                                    aSubNode.setTaxonomyID(id);
                                    nodesToAdd.add(aSubNode);
                                }
                            }
                        }
                    } else {
                        nodesToAdd.add(aMainNode);
                    }
                }
                node.setHasChildren(!nodesToAdd.isEmpty());
            }
        }
        return nodesToAdd;
    }

    public DefaultTaxonomyTreeModel openChildren(DefaultTaxonomyTreeNode node) throws MalformedURLException, IOException {
        DefaultTaxonomyTreeModel model = (DefaultTaxonomyTreeModel) this.getModel();
        List<DefaultTaxonomyTreeNode> nodesToAdd = new ArrayList<>();
        nodesToAdd = getChildren(node);
        //at this point, it is safe to add the nodes.
        for (DefaultTaxonomyTreeNode aNode : nodesToAdd) {
            if (searchNode(node, aNode.toString()) == null) {
                model.insertNodeInto(aNode, node, node.getChildCount());
            }
            node.setHasChildren(true);
            node.setWasExplored(false);
        }
        return model;
    }

    @Deprecated
    public DefaultTaxonomyTreeModel expandChildren(DefaultTaxonomyTreeNode node) throws MalformedURLException, IOException {
        DefaultTaxonomyTreeModel model = (DefaultTaxonomyTreeModel) this.getModel();
        //get the URL from the parent
        String currentURL = node.getURLAsString();
        //parse the document with JSOUP
        Document doc = Jsoup.connect(currentURL).get();
        // get page title
        List<DefaultTaxonomyTreeNode> nodesToAdd = new LinkedList<>();
        Elements ul = doc.select("ul");
        for (Element aTopList : ul) {
            for (Element aMainEntry : aTopList.select("a")) {
                if (aMainEntry != null) {
                    String id = aMainEntry.attr("href");
                    id = id.split("id=")[1];
                    id = id.substring(0, id.indexOf("&lv"));
                    String ulText = aMainEntry.text();
                    DefaultTaxonomyTreeNode aMainNode = new DefaultTaxonomyTreeNode(ulText);
                    aMainNode.setUrl(prefix + aMainEntry.attr("href"));
                    aMainNode.setTaxonomyID(id);
                    nodesToAdd.add(aMainNode);
                }
            }
        }
        //at this point, it is safe to add the nodes.
        for (DefaultTaxonomyTreeNode aNode : nodesToAdd) {
            if (searchNode(node, aNode.toString()) == null) {
                model.insertNodeInto(aNode, node, node.getChildCount());
            }
        }
        return model;
    }

    /**
     * This method is used when a search is performed or node is known without
     * its parent. This method checks for existing nodes in the tree and ents
     * the new node.
     *
     * @param jTree the species tree
     * @param exactSearch boolean indicating if this search is for an exact URL
     * or a searchTerm
     * @param term the term that was searched for OR the used URL
     * @throws MalformedURLException
     * @throws IOException
     */
    public DefaultTaxonomyTreeNode buildSearchedPathToRoot(String term, boolean exactSearch) throws MalformedURLException, IOException {
        DefaultTaxonomyTreeModel model = (DefaultTaxonomyTreeModel) getModel();
// need http protocol
        Document doc;
        if (exactSearch) {
            //if it's a suggested search, the term is a direct url
            doc = Jsoup.connect(term).get();
        } else {
            //else it's a searching term
            doc = Jsoup.connect("http://www.ncbi.nih.gov/Taxonomy/Browser/wwwtax.cgi?name=" + term.replace(" ", "%20") + "&lvl=1").get();
        }
// get page title
        Elements links = doc.select("a[href]");
        boolean addNodes = false;
        //list all the links that are related to species in the taxonomy browser
        List<DefaultTaxonomyTreeNode> listToPath = new LinkedList<>();
        for (Element aLink : links) {
            if (addNodes) {
                if (aLink.attr("href").contains("/Taxonomy/Browser/wwwtax.cgi?mode=Unde")
                        && !aLink.text().toLowerCase().contains("[help]")
                        && !aLink.text().toLowerCase().equals("here")) {

                    String id = aLink.attr("href");
                    id = id.split("id=")[1];
                    id = id.substring(0, id.indexOf("&lv"));
                    String ulText = aLink.text();
                    DefaultTaxonomyTreeNode newChild = new DefaultTaxonomyTreeNode(ulText);
                    newChild.setUrl(prefix + aLink.attr("href"));
                    newChild.setTaxonomyID(id);
                    listToPath.add(newChild);
                }
            }
            //only start these from lineage, so we get the full path
            if (aLink.text().equalsIgnoreCase("lineage")) {
                addNodes = !addNodes;
                //also try to add last one...
            } else if (aLink.attr("href").contains("/Taxonomy/Browser/wwwtax.cgi?mode=Info")) {
                String id = aLink.attr("href");
                id = id.split("id=")[1];
                id = id.substring(0, id.indexOf("&lv"));
                String ulText = aLink.text();
                if (!ulText.toLowerCase().contains("show organism")) {
                    DefaultTaxonomyTreeNode newChild = new DefaultTaxonomyTreeNode(ulText);
                    newChild.setUrl(prefix + aLink.attr("href"));
                    newChild.setTaxonomyID(id);
                    listToPath.add(newChild);
                    break;
                }
            }
        }
        //if no species were found, parse the suggestions
        if (listToPath.isEmpty() && !exactSearch) {
            //parse the webpage in other fashion to get the suggested species
            HashMap<String, String> possibleDirections = new HashMap<>();
            for (Element aLink : links) {
                if (aLink.attr("href").contains("Taxonomy/Browser") && !aLink.text().toLowerCase().contains("[help]")) {
                    possibleDirections.put(aLink.text(), prefix + aLink.attr("href"));
                }
            }
            //TODO check if this is correct !
            if (!possibleDirections.isEmpty()) {
                StringBuilder selectedValue = new StringBuilder();
                DialogSuggestedSearchTerms confirmationDialog = new DialogSuggestedSearchTerms(possibleDirections, selectedValue, this.parent, true);
                confirmationDialog.setVisible(true);
                if (selectedValue.toString().isEmpty() || selectedValue.toString().equalsIgnoreCase("cancelled")) {
                    return null;
                } else {
                    return buildSearchedPathToRoot(possibleDirections.get(selectedValue.toString()), true);
                }
            } else {
                //if there are no suggestions, throw an error
                throw new IOException("Could not find taxonomy for " + term);
            }
        }
        //if there were nodes found, add them to the tree
        DefaultTaxonomyTreeNode parent = (DefaultTaxonomyTreeNode) model.getRoot();
        DefaultTaxonomyTreeNode existingNode = null;
        //check if the last one exists, if not , go to pre-last one
        int index = listToPath.size();
        String currentNodeName;
        boolean nodeToEntOnFound = false;
        while (!nodeToEntOnFound) {
            index--;
            currentNodeName = listToPath.get(index).toString();
            existingNode = searchNode(parent, currentNodeName);
            if (existingNode != null) {
                nodeToEntOnFound = true;
            }
        }
        if (!nodeToEntOnFound) {
            existingNode = (DefaultTaxonomyTreeNode) model.getRoot();
        }
        //move the index up one because we now are in the actual parent of the node...
        index++;
        while (index < listToPath.size()) {
            DefaultTaxonomyTreeNode newChild = listToPath.get(index);
            model.insertNodeInto(newChild, existingNode, existingNode.getChildCount());
            existingNode = newChild;
            index++;
        }
        //find the children for the last path
        openChildren((DefaultTaxonomyTreeNode) existingNode);
        DefaultTaxonomyTreeNode searchNode = searchNode((DefaultTaxonomyTreeNode) existingNode, term);
        if (searchNode == null) {
            setSelectionPath(new TreePath(existingNode.getPath()));
            return existingNode;
        } else {
            setSelectionPath(new TreePath(searchNode.getPath()));
            return searchNode;
        }
    }

    public void collapseFullTree() {
        //collaps the tree and leave only open where the search result is
        for (int i = 0; i <= getRowCount(); i++) {
            collapseRow(i);
        }
    }

    public void collapseToNodeOnly(DefaultTaxonomyTreeNode node) {
        collapseFullTree();
        setSelectionPath(new TreePath(node.getPath()));
    }

    /**
     * initializes the species tree
     *
     */
    private void initTree() throws MalformedURLException {
        selectionModel.setSelectionMode(TreeSelectionModel.DISCONTIGUOUS_TREE_SELECTION);
        //remove default nodes
        setRootVisible(false);
        setShowsRootHandles(true);
        DefaultTaxonomyTreeNode root = new DefaultTaxonomyTreeNode("root");
        root.setUrl(prefix + "/wwwtax.cgi?mode=Root&id=1&lvl=1&lin=f&keep=1&srchmode=1&unlock");
        DefaultTaxonomyTreeNode cellularOrganisms = new DefaultTaxonomyTreeNode("cellular organisms");
        DefaultTaxonomyTreeModel lineageTreeModel = new DefaultTaxonomyTreeModel(cellularOrganisms);
        taxonomyTreeIconRenderer = new TaxonomyTreeIconRenderer(this);
        setCellRenderer(taxonomyTreeIconRenderer);
        root.setHasChildren(true);
        lineageTreeModel.setRoot(cellularOrganisms);
        cellularOrganisms.setHasChildren(true);
        String[] primordials = new String[]{
            "Archaea",
            "Bacteria",
            "Eukaryota",
            "Viroids",
            "Viruses",
            "Other",
            "Unclassified"};
        for (String aPrimordial : primordials) {
            DefaultTaxonomyTreeNode primordialNode = new DefaultTaxonomyTreeNode(aPrimordial);
            primordialNode.setIconName(aPrimordial.substring(0, 2).toUpperCase());
            primordialNode.setUrl(prefix + "/Taxonomy/Browser/wwwtax.cgi?name=" + aPrimordial + "&id=1&lvl=1&lin=f&keep=1&srchmode=1&unlock");
            lineageTreeModel.insertNodeInto(primordialNode, cellularOrganisms, cellularOrganisms.getChildCount());
        }
        setModel(lineageTreeModel);
        //forceLookAndFeel();
        repaint();
        revalidate();
    }

    private void forceLookAndFeel() {
        try {
            NimbusLookAndFeel laf = new NimbusLookAndFeel();
            UIManager.setLookAndFeel(laf);
            UIDefaults newUIDefaults = laf.getDefaults();
            newUIDefaults.put("Tree.drawHorizontalLines", true);
            newUIDefaults.put("Tree.drawVerticalLines", true);
            newUIDefaults.put("Tree.showRootHandles", false);
            newUIDefaults.put("Tree.dropLineColor", "#d6d9df (214,217,223)");
            newUIDefaults.put("Tree.line", "#d6d9df (214,217,223)");
            for (javax.swing.UIManager.LookAndFeelInfo info : javax.swing.UIManager.getInstalledLookAndFeels()) {
                if ("nimbus".equals(info.getName())) {
                    javax.swing.UIManager.setLookAndFeel(info.getClassName());
                    break;
                }
            }
        } catch (ClassNotFoundException | InstantiationException | IllegalAccessException | UnsupportedLookAndFeelException ex) {
            Logger.getLogger(NCBITaxonomyViewer.class.getName()).log(Level.SEVERE, null, ex);
        }
    }

    @Override
    public void scrollPathToVisible(TreePath path) {
        super.scrollPathToVisible(path);
        if (path != null) {
            setSelectionPath(path);
        }
    }

    protected DefaultTaxonomyTreeNode getCommonAncestor(TreePath path1, TreePath path2) {
        DefaultTaxonomyTreeNode commonAncestor = null;
        for (Object aNodeInPath1 : path1.getPath()) {
            for (Object aNodeInPath2 : path2.getPath()) {
                if (((TreeNode) aNodeInPath1).toString().equalsIgnoreCase(((TreeNode) aNodeInPath2).toString())) {
                    commonAncestor = (DefaultTaxonomyTreeNode) aNodeInPath1;
                }
            }
        }
        if (commonAncestor == null) {
            commonAncestor = (DefaultTaxonomyTreeNode) getModel().getRoot();
        }
        return commonAncestor;
    }

    public DefaultTaxonomyTreeNode getCommonAncestor() {
        DefaultTaxonomyTreeNode commonAncestor = (DefaultTaxonomyTreeNode) getModel().getRoot();
        TreePath[] selectionPaths = getSelectionPaths();
        TreePath aFirstSelectedPath = selectionPaths[0];
        for (TreePath aSelectedPath : getSelectionPaths()) {
            commonAncestor = getCommonAncestor(aFirstSelectedPath, aSelectedPath);
            aFirstSelectedPath = new TreePath(getCommonAncestor(aFirstSelectedPath, aSelectedPath).getPath());
        }
        clearSelection();
        addSelectionPath(new TreePath(new TreePath(commonAncestor.getPath())));
        collapseToNodeOnly(commonAncestor);
        repaint();
        revalidate();
        return commonAncestor;
    }

}
